/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Wilco.
 *
 * The Initial Developer of the Original Code is Mozilla.
 * Portions created by the Initial Developer are Copyright (C) 2008
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

/**
 * This module provides parsing and unparsing of DMAP tagged strings
 *
 *   let MyExtension = {
 *     DMAP: null,
 *     ...
 *   };
 *   Components.utils.import("chrome://myextension/modules/DMAP.js", MyExtension);
 *   // Now MyExtension.DMAP is an object implementing parse and stringify, a la Gecko API

 */

let EXPORTED_SYMBOLS = ["DMAP"];

var DMAP = {
	 _contentCodes: 
	{
		//Example of content code
		//mstt: { type: 'int',	name: 'dmap.status' },
	},
	load_mappings: function(dmapString)
	{
	},
	parse: function(byteArray) 
	{
		
		var index = this.parseString(byteArray.splice(0,4));
		code = this._contentCodes[index];
		dump('Found code: ' + index + ' ');
		var obj = {};
		
		var length = this.parseInt(byteArray.splice(0,4));
		
		if (length > byteArray.length)		//iTunes may report too long a list sometimes
			length = byteArray.length;
			
		var data = byteArray.splice(0,length);
		
		dump('Length: ' + length.toString() + '\n');
		if (code == undefined) return obj;
		
		obj[index] = this.parseDMAPNode(data,code);
		return obj;
	},
	parseDMAPNode: function(byteArray,code)
	{	
		switch (code.type)
		{
			case 'list':
				var obj = {};
				while(byteArray.length > 0)
				{
					var index = this.parseString(byteArray.splice(0,4));
					var fieldCode = this._contentCodes[index];
					dump('Found code: ' + index + '\n');
					var length = this.parseInt(byteArray.splice(0,4));
					
					if (length > byteArray.length)		//iTunes may report too long a list sometimes
						length = byteArray.length;
						
					var data = byteArray.splice(0,length);
					
					if (code == undefined) continue;
					
					if (obj[index] != undefined)
					{	//Create an array
						var temp = obj[index];
						obj[index] = [];
						obj[index].push(temp);
						obj[index].push(this.parseDMAPNode(data,fieldCode));
					}
					else
						obj[index] = this.parseDMAPNode(data,fieldCode);
				}
				return obj;
			case 'int':
			case 'date':
				return this.parseInt(byteArray);
			case 'short':
				return this.parseShort(byteArray);
			case 'long':
				return this.parseLong(byteArray);
			case 'string':
				return this.parseString(byteArray);
			case 'byte':
				return this.parseByte(byteArray[0]);
			case 'version':
				return this.parseVersion(byteArray);
			case 'hex':
				return this.parseHex(byteArray);
		}
		//default
		return this.parseString(byteArray);
	},
	parseByte: function(aByte)
	{
		var value = 0;
		value = parseInt(aByte);
		return value;
	},
	parseInt: function(byteArray)
	{
		var value = 0;
		value = parseInt(byteArray[0]);
		value = value<<8;
		value = value + parseInt(byteArray[1]);
		value = value << 8;
		value = value + parseInt(byteArray[2]);
		value = value << 8;
		value = value + parseInt(byteArray[3]);
		return value;
	},
	parseString: function(byteArray)
	{
		var str = '';
		for(i=0;i<byteArray.length;i++)
			str += String.fromCharCode(byteArray[i]);
			
		return str;
	},
	parseShort: function(byteArray)
	{
		var value = 0;
		value = parseInt(byteArray[0]);
		value = value<<8;
		value = value + parseInt(byteArray[1]);
	},
	parseLong: function(byteArray)
	{
		var value = 0;
		value = parseInt(byteArray[0]);
		value = value<<8;
		value = value + parseInt(byteArray[1]);
		value = value << 8;
		value = value + parseInt(byteArray[2]);
		value = value << 8;
		value = value + parseInt(byteArray[3]);
		value = value << 8;
		value = value + parseInt(byteArray[4]);
		value = value << 8;
		value = value + parseInt(byteArray[5]);
		value = value << 8;
		value = value + parseInt(byteArray[6]);
		value = value << 8;
		value = value + parseInt(byteArray[7]);
		return value;
	},
	parseVersion: function(byteArray)
	{
		var str = '';
		for(i=0;i<byteArray.length;i++)
			str += String.fromCharCode(byteArray[i] + 48);
		
		return str;
	},
	parseHex: function(byteArray)
	{
		var str = '';
		for(i=0;i<byteArray.length;i++)
		{
			var val = byteArray[i];
			
			var num = (val >> 4);
			num = num + (( num > 9) ? 55 : 48);
			var first = String.fromCharCode(num);
			
			num = (val % 16);
			num = num + (( num > 9) ? 55 : 48);
			var second = String.fromCharCode(num);
			
			str += first + second;
		}
		return str;
	},
	stringify: function(jsObject) 
	{ 
		var len = this.countDMAPLength(jsObject);
		var output = new Array(len);
		var j = 0;
		this.stringifyDMAPNode(jsObject,output,j);
		return output;
	},
	countDMAPLength: function(node)
	{
		var count = 0;
		var code = null;
		for (var index in node)
		{
			code = this._contentCodes[index];
				
			if (code == undefined)
			{
				dump('Unknown DMAP code: ' + index + '\n');
				//ignore unknown codes
				continue;
			}
			
			if (code.isArray)
			{
				for(var k=0;k<node[index].length;k++)
				{
					count = count + this.countDMAPLength(node[index][k]);
				}
			}
			else
					count = count + this.countDMAPNodeLength(node[index],code,index);
		}
		
		return count;
		
	},
	countDMAPNodeLength: function(data,code,name)
	{
		var count;
		switch (code.type)
		{
			case 'list':
				if (code.name == 'dmap.listingitem' && typeof(data) == 'string')
				{	//HACK for string mlit
					count = 8 + data.length;
					break;
				}
				count = this.countDMAPLength(data) + 8;
				break;
			case 'int':
			case 'date':
				count = 12;
				break;
			case 'short':
				count = 10;
				break;
			case 'long':
				count = 16;
				break;
			case 'byte':
				count = 9;
				break;
			case 'string':
				count = 8 + data.length;
				break;
			case 'version':
				count = 12;
				break;
			case 'hex':
				count = 8 + data.length/2;
				break;
			case 'null':
				count = 8;
				break;
			default:
				dump('Unknown type: ' + code.type);
				count = 0;
				break;
			
		}
		return count;
	},
	stringifyDMAPNode: function(r,output,j)
	{
		var code = undefined;
		for (var index in r)
		{
			code = this._contentCodes[index];		
			j = this.stringifyDMAPData(r[index],index,code,output,j);
		}
		return j;
	},
	stringifyDMAPData: function(data,name,code,output,j)
		//Handle arrays
	{
		if (data[0] != undefined && data.length != undefined && typeof(data) == 'object')
		{
			//an array
			for (var k=0;k<data.length;k++)
			{
				j = this.stringifyDMAPData(data[k],name,code,output,j);
			}
			return j;
		}
		
		if (code == undefined)
		{	//Ignore unknown codes
			return j;
		}
		//Write node name
		for (var i=0;i<4;i++)
		{
			output[j] = name.charCodeAt(i);
			j++;
		}
			
		//HACK! Sometimes an mlit node can be a string, fucking apple!
		if (code.name == 'dmap.listingitem' && typeof(data) == 'string') {
			j = this.writeStringNode(data,output,j);
			return j;
		}
		switch (code.type)
		{
			case 'list':
				var k = j;
				j = j + 4;
				j = this.stringifyDMAPNode(data,output,j);
				this.writeInt(j-k-4,output,k);
				break;
			case 'int':
			case 'date':
				j = this.writeIntNode(data,output,j);
				break;
			case 'short':
				j = this.writeShortNode(data,output,j);
				break;
			case 'long':
				j = this.writeLongNode(data,output,j);
				break;
			case 'byte':
				j = this.writeByteNode(data,output,j);
				break;
			case 'string':
				j = this.writeStringNode(data,output,j);
				break;
			case 'version':
				j = this.writeInt(4,output,j);
				for (i=0;i<4;i++)
				{
					output[j] = data.charCodeAt(i) - 48;
					j++;
				}
				break;
			case 'hex':
				j = this.writeHexNode(data,output,j);
				break;
			case 'null':
				j = this.writeInt(0,output,j);
				break;
			default:
				dump('Unknown type: ' + code.type);
				break;
				
		}
		return j;
	},
	writeInt: function(x,output,j) {
		output[j+3] = x & (255);
		x = x >> 8;
		output[j+2] = x & (255);
		x = x >> 8;
		output[j+1] = x & (255);
		x = x >> 8;
		output[j] = x & (255);
		return j+4;
	},
	writeHex: function(data,output,j) {
		var offset = 0;
		data = data.toUpperCase();
		for (i=0;i<data.length;i++)
		{
			var valueFirst = data.charCodeAt(i);
			valueFirst = valueFirst - ((valueFirst > 64) ? 55 : 48);
			var valueSecond = data.charCodeAt(++i);
			valueSecond = valueSecond - ((valueSecond > 64) ? 55 : 48);
			
			value = valueFirst >> 4;
			value = value + valueSecond;
			output[j+offset] = value;
			offset++;
		}
		return j+offset;
	},
	writeIntNode: function(x,output,j) {
		j = this.writeInt(4,output,j);
		j = this.writeInt(x,output,j);
		return j;
	},
	writeHexNode: function(x,output,j) {
		j = this.writeInt(x.length/2,output,j)
		j = this.writeHex(x,output,j);
		return j;
	},
	writeLongNode : function(x,output,j) {
		j = this.writeInt(8,output,j);
		
		output[j+7] = x & (255);
		x = x >> 8;
		output[j+6] = x & (255);
		x = x >> 8;
		output[j+5] = x & (255);
		x = x >> 8;
		output[j+4] = x & (255);
		x = x >> 8;
		output[j+3] = x & (255);
		x = x >> 8;
		output[j+2] = x & (255);
		x = x >> 8;
		output[j+1] = x & (255);
		x = x >> 8;
		output[j] = x & (255);
		return j+8;
	},
	writeShortNode : function(x,output,j) {
		j = this.writeInt(2,output,j);
		output[j+1] = x & (255);
		x = x >> 8;
		output[j] = x & (255);
		return j+2;
	},
	writeByteNode : function(x,output,j) {
		j = this.writeInt(1,output,j);
		if (x)
			output[j] = x & (255);
		else
			output[j] =  0 & (255);
			
		return j+1;
	},
	writeStringNode: function(data,output,j){
		j = this.writeInt(data.length,output,j);
		for (i=0;i<data.length;i++)
		{
			output[j] = data.charCodeAt(i);
			j++;
		}
		return j;
	},
	codeToInt : function(code)
	{
		var num = 0;
		num = num + code.charCodeAt(0);
		num =  num << 8;
		num = num + code.charCodeAt(1);
		num =  num << 8;
		num = num + code.charCodeAt(2);
		num =  num << 8;
		num = num + code.charCodeAt(3);
		
		return num;
	},
	typeToShort : function (type)
	{
		switch(type)
		{
			case 'byte':
				return 1;
			case 'short':
				return 3;
			case 'int':
				return 5;
			case 'long':
			case 'hex':
				return 7;
			case 'string':
				return 9;
			case 'date':
				return 10;
			case 'version':
				return 11;
			case 'list':
				return 12;
		}
	},
	print: function(dmapObject)
	{
			//prints a readable version of the output
			var linePrefix = '';
			return this.printNode(dmapObject,linePrefix);
	},
	printNode: function(dmapNode,linePrefix)
	{
		str = '';
		for (index in dmapNode)
		{
			code = this._contentCodes[index];
			str += this.printField(index,code,dmapNode[index],linePrefix + '   ');
		}
		return str;
	},
	printField: function(index,code,data,linePrefix)
	{
		var str = '';
		//Handle arrays
		if (data[0] != undefined && data.length != undefined && typeof(data) == 'object')
		{
			for (var k=0;k<data.length;k++)
			{
				str = str + this.printField(index,code,data[k],linePrefix);
			}
			return str;
		}
		
		if (code == undefined)
		{
			str = str + linePrefix + index + ': UNDEFINED\n';
			return str;
		}
		//Handle nodes
		
		//Hacks go here
		//HACK! Sometimes an mlit node can be a string, fucking apple!
		if (code.name == 'dmap.listingitem' && typeof(data) == 'string')
		{
			str = str + linePrefix + index + '   ' + data.length + '  ' + data + '\n';
			return str;
		}
		
		switch (code.type)
		{
			case 'list':
				str = str + linePrefix + index + ' --+\n';
				str += this.printNode(data,linePrefix + '   ');
				return str;
			case 'null':
				str = str + linePrefix + index + '   0\n'; 
				return str;
			case 'int':
			case 'date':
			case 'version':
			case 'short':
			case 'long':
			case 'byte':
			case 'string':
			case 'hex':
				str = str + linePrefix + index + '   ';
				switch(code.type)
				{
					case 'int':
					case 'date':
					case 'version':
						str = str + '4   ';
						break;
					case 'short':
						str = str + '2   ';
						break;
					case 'long':
						str = str + '8   ';
						break;
					case 'byte':
						str = str + '1   ';
						break;
					case 'string':
						str = str + data.length + '  ';
						break;
					case 'hex':
						str = str + data.length/2 + '  ';
						break;
					
				}
				str = str + data.toString() + '\n';
				return str;
			default:
				str = str + linePrefix + index + ': UNKNOWN TYPE ' + code.type + '\n';
				return str;
		}
		return str;
	},
  }

